import * as execa from 'execa';
import fse from 'fs-extra';
import JSON5 from 'json5';
import os from 'os';
import path, { resolve } from 'path';

import { runHvigorAsync } from '../../common/common';
import { commonConfig } from '../common-config';
import { commonUtil } from '../common-util';
import { IsEnable, SignType, TaskStatus } from '../project-builder/models/constants';
import { ProjectTemplateOption } from '../project-builder/models/model-options';
import { Project } from '../project-builder/models/project-models';
import { copyTmp } from '../project-builder/models/template-models';
import { ModulesOption } from '../project-builder/project-builder';
import { DefaultTemplate } from '../project-builder/template/template-init';
import { reportBuilder } from '../report-builder/report-builder';
import { ICaseRecord, taskReport } from '../report-builder/report-models';

import { TestTaskOption } from './test-task-option';

/**
 * 签名文件配置
 *
 **/
const signatureTemplate = [
  {
    'name': 'default',
    'material': {
      storeFile: './sign/myapplication_debug.p12',
      storePassword: '0000001B734C857D79A0AFE684D4F7C9461064D24605328C9AC15302BCB7D9606F8B6D82949497240F684F',
      keyAlias: 'debugKey',
      keyPassword: '0000001B92B7D7143F022695515F8F862090F22016F59D86A642F36AA953ECD76A5DAE6941C04D8FFFF864',
      signAlg: 'SHA256withECDSA',
      profile: './sign/myapplication_debug.p7b',
      certpath: './sign/myapplication_debug.cer'
    }
  },
  {
    'name': 'hos',
    'material': {
      storeFile: './sign-hos/myapplication_debug.p12',
      storePassword: '0000001B2D179B0F5D0EA8CFB3C994FC5182AEF261E80725CECDA0E55D3F312EC1691D321D0C918672CBB5',
      keyAlias: 'debugKey',
      keyPassword: '0000001BAF50B2DA8243ECC645AE9825D734D180624F77D96E7B36EF287539B7BDDD5EF7B1E6049FC9E817',
      signAlg: 'SHA256withECDSA',
      profile: './sign-hos/myapplication_debug.p7b',
      certpath: './sign-hos/myapplication_debug.cer'
    },
    'type': 'HarmonyOS'
  }
];

/**
 * 签名文件夹
 *
 * @type {FileCopyTemplate}
 */
const signDir = copyTmp('/common/sign');
const signHosDir = copyTmp('/common/sign-hos');

/**
 * 用例构建工程的配置
 */
export type ProjectBuildOption = {
  template: ProjectTemplateOption,
  modulesOption?: ModulesOption
}
const defaultProjectName = 'myapplication';

/**
 * 测试用例函数类型
 */
export type TestCaseFun = (project: Project, taskConfig: TestTaskOption, record: ICaseRecord) => Promise<void>;

/**
 * 测试用例类
 */
export class TestCase {

  /**
   * 任务名称
   */
  public caseName: string;

  /**
   * 任务描述
   */
  public description?: string;

  /**
   * 测试工程对象构建方法
   */
  public projectBuilder: ProjectBuildOption;

  /**
   * 测试任务属性
   */
  public taskConfig: TestTaskOption;

  /**
   * 测试任务过程
   *
   * @param {ICaseRecord} record
   */
  public testFun: TestCaseFun;

  /**
   * 构造函数
   *
   * @param {string} name 用例名称
   * @param {ProjectBuildOption} builder 工程构造方法|工程类型选项（用于从默认模板中查找对应工程）
   * @param {TestTaskOption} taskConfig
   * @param {TestCaseFun} testFun
   * @param description
   */
  constructor(name: string, builder: ProjectBuildOption, taskConfig: TestTaskOption, testFun: TestCaseFun,
    description?: string) {
    this.caseName = name;
    this.projectBuilder = builder;
    this.taskConfig = taskConfig;
    this.testFun = testFun;
    this.description = description;
  }

  /**
   * 执行用例
   *
   * @param {string} tempPath 临时文件路径
   * @return {Promise<void>}
   */
  public async run(tempPath: string): Promise<void> {
    // 生成本用例工程所在的随机文件夹
    const randomDir = path.join(tempPath, commonUtil.genRandomString(6));
    await fse.ensureDir(randomDir);

    // 初始化执行记录对象
    const record = await this.initRecord(randomDir);

    // 生成工程对象
    const project: Project = await this.initProject(randomDir);

    // 生成签名文件，下载ohpm依赖
    await Promise.all([this.initSign(project), this.initOhpmDependencies(project)]);
    try {
      // 执行任务
      await this.testFun(project, this.taskConfig, record);
      record.taskStatus = TaskStatus.Pass;
    } catch (e: any) {
      record.errorMessage = commonUtil.removeANSI(
        (e as any)['shortMessage'] ?? this.truncateAtThirdNewline(e.message));
      record.taskStatus = TaskStatus.Fail;
      if (e.stdout) {
        record.addStep('execa执行命令', {
          stdout: e.stdout,
          stderr: e.stderr
        });
      }
      throw e;
    } finally {
      // 关闭daemon服务进程
      try {
        await runHvigorAsync(project.projectRootPath(), ['--stop-daemon']);
      } catch (e: any) {
        console.log(e);
      }

      // 设置任务结束时间，并输出报告
      record.endTime = new Date(Date.now());
      if (record.taskStatus === TaskStatus.Running) {
        if (record.results.length === 0) {
          record.taskStatus = TaskStatus.NotExecuted;
        } else {
          record.taskStatus = TaskStatus.Fail;
        }
      }
      if (taskReport.isWindows) {
        // 写出日志文件
        const consoleLogPath = path.join(record.rootPath!, 'console.log');
        const errorLogPath = path.join(record.rootPath!, 'error.log');
        record.consoleLogPath = consoleLogPath;
        record.errorLogPath = errorLogPath;
        for (const [step, result] of record.results) {
          await fse.appendFile(consoleLogPath, `==========================${step}==========================${os.EOL}`);
          await fse.appendFile(consoleLogPath, commonUtil.removeANSI(result.stdout));
          await fse.appendFile(consoleLogPath, `${os.EOL}${os.EOL}${os.EOL}`);
          await fse.appendFile(errorLogPath, `==========================${step}==========================${os.EOL}`);
          await fse.appendFile(errorLogPath, commonUtil.removeANSI(result.stderr));
          await fse.appendFile(errorLogPath, `${os.EOL}${os.EOL}${os.EOL}`);
        }
        await reportBuilder.buildCaseReport(record);
      }
    }
  }

  /**
   * 初始化用例执行记录对象
   *
   * @param {string} randomDir
   * @return {Promise<ICaseRecord>}
   * @private
   */
  private async initRecord(randomDir: string): Promise<ICaseRecord> {
    const record = taskReport.caseRecord(this.caseName);
    record.description = this.description;
    record.rootPath = path.join(taskReport.rootPath!, 'cases', this.caseName);
    await fse.ensureDir(record.rootPath);
    record.reportPath = path.join(record.rootPath, `report_${this.caseName}.html`);
    record.taskStatus = TaskStatus.Running;
    record.startTime = new Date(Date.now());
    record.projectPath = randomDir;
    return record;
  }

  /**
   * 生成工程文件
   *
   * @param {string} randomDir
   * @return {Promise<Project>}
   * @private
   */
  private async initProject(randomDir: string): Promise<Project> {
    const project: Project = DefaultTemplate.project(this.projectBuilder.template,
      this.projectBuilder.modulesOption)(defaultProjectName, this.projectBuilder.template);

    // 生成工程文件
    await project.build(randomDir);
    return project;
  }

  /**
   * 加载签名文件
   *
   * @param {Project} project
   * @return {Promise<void>}
   * @private
   */
  private async initSign(project: Project): Promise<void> {
    if (this.taskConfig.sign === SignType.NoSign) {
      return;
    }

    // 读取build-profile.json5 文件内容
    const buildJsonPath = project.dirNode!.node('build-profile.json5').path;
    const fileData = await fse.readFile(buildJsonPath);

    const buildProfileObj = JSON5.parse(fileData.toString());

    // 修改build-profile.json5 文件内容，添加签名配置
    buildProfileObj.app.signingConfigs = signatureTemplate;

    // 分别执行文件复制和 build-profile.json5 文件内容重写
    const projectPath = project.dirNode!.path;
    await Promise.all([
      signDir.generated(projectPath),
      signHosDir.generated(projectPath),
      fse.writeJson(buildJsonPath, buildProfileObj, { spaces: 2 })
    ]);
  }

  /**
   * 加载ohpm依赖
   *
   * @param {Project} project
   * @return {Promise<void>}
   * @private
   */
  private async initOhpmDependencies(project: Project): Promise<void> {
    if (this.taskConfig.ohpmInstall === IsEnable.DisAble) {
      return;
    }

    // 读取build-profile.json5 文件内容
    const projectPath = project.dirNode!.path;
    execa.commandSync(commonConfig.ohpmInstallCommand, {
      cwd: projectPath
    });
  }

  private truncateAtThirdNewline(s: string): string {
    let newlineCount = 0;
    for (let i = 0; i < s.length; i++) {
      if (s[i] === '\n') {
        newlineCount++;
      }
      if (newlineCount === 3) {
        return s.slice(0, i);
      }
    }
    return s;
  }
}
